/*
 * Copyright (c) 2008, Takashi Ito
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of the authors nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#include "tansu_internal.h"
#include "tansu_status.h"
#include "tansu_util.h"
#include "tansu_upnp.h"
#include "tansu_upnp_cp.h"
#include "tdom.h"

struct tansu_cp_discovery *
tansu_cp_discovery_new(struct event_base *evbase,
                       const char *udn,
                       const struct tansu_ssdp_packet *packet)
{
    struct tansu_cp_discovery *discovery;
    struct tansu_keyval *keyval;
    char *address;
    uint16_t port;

    if ((discovery = calloc(1, sizeof(struct tansu_cp_discovery))) == NULL)
        return NULL;

    discovery->state = TANSU_SSDP_STATE_CREATED;
    discovery->last_status = -1;
    discovery->jobs = 0;
    if (udn && (discovery->udn = strdup(udn)) == NULL)
        goto out;

    if (packet == NULL)
        return discovery;

    if ((discovery->packet = tansu_ssdp_packet_clone(packet)) == NULL)
        goto out;

    if ((keyval = tansu_keyvals_get(&discovery->packet->headers, "Location")) == NULL)
        goto out;

    if (tansu_url_isaddr(keyval->value, &discovery->packet->remote) == 0)
        goto out;

    if ((address = tansu_socket_getaddress(&discovery->packet->remote)) == NULL)
        goto out;
    if ((port = tansu_url_getport(keyval->value)) == 0)
        port = 80;

    discovery->conn = evhttp_connection_base_new(evbase, NULL, address, port);
    free(address);
    if (discovery->conn == NULL)
        goto out;

    discovery->location = keyval->value;
    return discovery;

out:
    tansu_cp_discovery_delete(discovery);
    return NULL;
}

void
tansu_cp_discovery_delete(struct tansu_cp_discovery *discovery)
{
    tansu_ssdp_packet_delete(discovery->packet);
    if (discovery->conn)
        evhttp_connection_free(discovery->conn);
    free(discovery->udn);
    free(discovery);
}

tansu_status_t
tansu_http_get(struct evhttp_connection *conn,
               const char *path,
               struct evkeyvalq *headers,
               void cb(struct evhttp_request *, void *),
               void *cbarg)
{
    struct evkeyval *kv;
    struct evhttp_request *req;
    char *address;
    uint16_t port;

    if (conn == NULL || path == NULL)
        return TANSU_INVALID_ARGUMENT;

    if ((req = evhttp_request_new(cb, cbarg)) == NULL)
        return TANSU_MEMORY_ERROR;

    // TODO: chunk support
    //evhttp_request_set_chunked_cb(req, chunked_cb);

    evhttp_connection_get_peer(conn, &address, &port);
    if ((evhttp_add_header(req->output_headers, "Host", address)) != 0)
        goto out;

    if (headers) {
        TAILQ_FOREACH(kv, headers, next) {
            if ((evhttp_add_header(req->output_headers, kv->key, kv->value)) != 0)
                goto out;
        }
    }

    if (evhttp_make_request(conn, req, EVHTTP_REQ_GET, path) != 0)
        goto out;

    return TANSU_SUCCESS;

out:
    evhttp_request_free(req);
    return TANSU_MEMORY_ERROR;
}

tansu_status_t
tansu_http_request(struct evhttp_connection *conn,
                   const char *method,
                   const char *path,
                   void cb(struct evhttp_request *, void *),
                   void *cbarg,
                   struct evhttp_request **request)
{
    struct evhttp_request *req;
    enum evhttp_cmd_type type;
    char *address;
    uint16_t port;

    if (conn == NULL || method == NULL || path == NULL)
        return TANSU_INVALID_ARGUMENT;

    if (strcmp("GET", method) == 0)
        type = EVHTTP_REQ_GET;
    else if (strcmp("HEAD", method) == 0)
        type = EVHTTP_REQ_HEAD;
    else if (strcmp("POST", method) == 0)
        type = EVHTTP_REQ_POST;
    else
        return TANSU_NOT_SUPPORTED;

    if ((req = evhttp_request_new(cb, cbarg)) == NULL)
        return TANSU_MEMORY_ERROR;

    // TODO: chunk support
    //evhttp_request_set_chunked_cb(req, chunked_cb);

    evhttp_connection_get_peer(conn, &address, &port);
    if ((evhttp_add_header(req->output_headers, "Host", address)) != 0)
        goto out;

    if (evhttp_make_request(conn, req, type, path) != 0)
        goto out;

    if (request)
        *request = req;
    return TANSU_SUCCESS;

out:
    evhttp_request_free(req);
    return TANSU_MEMORY_ERROR;
}

struct tansu_cp_discovery *
tansu_cp_discoveries_get(struct tansu_cp_discoveries *list, const char *udn)
{
    struct tansu_cp_discovery *discovery;

    if (list == NULL || udn == NULL)
        return NULL;

    STAILQ_FOREACH(discovery, list, next) {
        if (strcmp(udn, discovery->udn) == 0)
            return discovery;
    }

    return NULL;
}

struct tansu_cp_service *
tansu_cp_service_new(const char *type, const char *sid, struct tdom_doc *scpd)
{
    struct tansu_cp_service *service = NULL;

    service = calloc(1, sizeof(struct tansu_cp_service));
    if (service == NULL)
        return NULL;

    if ((service->type = strdup(type)) == NULL)
        goto out;

    if ((service->sid = strdup(sid)) == NULL)
        goto out;

    service->scpd = scpd;

    return service;

out:
    tansu_cp_service_delete(service);
    return NULL;
}

void tansu_cp_service_delete(struct tansu_cp_service *service)
{
    if (service == NULL)
        return;

    free(service->type);
    free(service->sid);
    tdom_doc_delete(service->scpd);
    free(service);
}

struct tansu_cp_device *tansu_cp_device_new(const char *udn,
                                            const char *location,
                                            struct tdom_doc *doc)
{
    struct tansu_cp_device *device;
    struct tdom_elem *elem;
    struct tdom_elem *tmp = NULL;
    const char *p;
    int i;
    int n;

    if (doc == NULL || udn == NULL)
        return NULL;

    if ((device = calloc(1, sizeof(struct tansu_cp_device))) == NULL)
        return NULL;

    elem = tdom_get_elem(doc->root, "device", TANSU_NS_UPNP_DEVICE, 0);
    device->udn = tdom_get_value(tdom_get_elem(elem, "UDN", TANSU_NS_UPNP_DEVICE, 0));
    if (device->udn == NULL)
        goto out;
    device->rootdevice = elem;
    if ((device->location = strdup(location)) == NULL)
        goto out;

    /* for embedded */
    if (strcmp(udn, device->udn) == 0) {
        device->embedded = NULL;
    } else {
        elem = tdom_get_elem(elem, "deviceList", TANSU_NS_UPNP_DEVICE, 0);
        if (elem == NULL)
            goto out;
        n = tdom_count_elem(elem, "device", TANSU_NS_UPNP_DEVICE);
        for (i = 0; i < n; i++) {
            if ((tmp = tdom_get_elem(elem, "device", TANSU_NS_UPNP_DEVICE, i)) == NULL)
                continue;
            p = tdom_get_value(tdom_get_elem(tmp, "UDN", TANSU_NS_UPNP_DEVICE, 0));
            if (p == NULL || strcmp(p, udn) != 0)
                continue;
            device->embedded = tmp;
            device->udn = p;
            break;
        }
    }

    device->dd = doc;
    STAILQ_INIT(&device->services);
    return device;

out:
    tansu_cp_device_delete(device);
    return NULL;
}

void tansu_cp_device_delete(struct tansu_cp_device *device)
{
    //struct tansu_cp_device *embedded; // TODO
    struct tansu_cp_service *service;

    if (device == NULL)
        return;

    STAILQ_FREE_ALL(service, &device->services, next, tansu_cp_service_delete);

    tdom_doc_delete(device->dd);
    free(device->location);
    free(device);
}

char *tansu_url_get_abs(const char *location, const char *base, const char *relative)
{
    const char *tmp;
    size_t n;
    size_t len;
    char *buff;

    if (relative == NULL)
        return NULL;

    if (strstr(relative, "://"))
        return strdup(relative);

    if (base == NULL) {
        if (location == NULL)
            return NULL;
        base = location;
    }

    if ((tmp = strstr(base, "://")) == NULL)
        return NULL;

    if ((tmp = strchr(&tmp[3], '/'))) { /* e.g. http://192.168.0.1:80/path/to/what */
#if 0
        for (;;) {
            buff = strchr(&tmp[1], '/');
            if (buff == NULL)
                break;
            tmp = buff;
        }
#endif
        if (relative[0] == '/')
            relative++;
        n = tmp - base + 1;
        len = n + strlen(relative) + 1;
        if ((buff = malloc(len))) {
            memcpy(buff, base, n);
            snprintf(&buff[n], len - n, "%s", relative);
        }
    } else {                            /* e.g. http://192.168.0.1:80 */
        len = strlen(base) + strlen(relative) + 1;
        const char *fmt = "%s%s";       /* e.g. /path/to/what */
        if (relative[0] != '/') {       /* e.g. path/to/what */
            fmt = "%s/%s";
            len += 1; 
        }
        if ((buff = malloc(len)))
            snprintf(buff, len, fmt, base, relative);
    }

    return buff;
}

struct tdom_doc *tansu_xml_parse(const char *buff, int buffsize)
{
    struct tdom_parser *parser;
    struct tdom_doc *doc;

    if (buff == NULL || buffsize < 4) /* <a/> */
        return NULL;

    if ((parser = tdom_parser_new()) == NULL)
        return NULL;

    if (tdom_parser_parse(parser, buff, buffsize) != 1) {
        tdom_parser_delete(parser);
        return NULL;
    }

    doc = tdom_parser_get_doc(parser);
    tdom_parser_delete(parser);

    return doc;
}

struct evkeyvalq *tansu_evkeyvalq_clone(const struct evkeyvalq *keyvals)
{
    struct evkeyval *keyval;
    struct evkeyvalq *copy;

    if (keyvals == NULL)
        return NULL;

    if ((copy = malloc(sizeof(struct evkeyvalq))) == NULL)
        return NULL;

    TAILQ_INIT(copy);

    TAILQ_FOREACH(keyval, keyvals, next) {
        if (evhttp_add_header(copy, keyval->key, keyval->value) != 0)
            goto out;
    }

    return copy;

out:
    evhttp_clear_headers(copy);
    free(copy);
    return NULL;
}

